Skip to content

refactor(common-adapters): give each popup mode its own module - #29609

Open
chrisnojima wants to merge 2 commits into
masterfrom
nojima/HOTPOT-arch-05-popup-modes
Open

chrisnojima wants to merge 2 commits into
masterfrom
nojima/HOTPOT-arch-05-popup-modes

Conversation

@chrisnojima

@chrisnojima chrisnojima commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem

PopupProps had 16 fields, and popup/index.tsx dispatched on runtime state into four presentation modes. Most props were dead in any given mode:

Mode Props read Props silently ignored
Portal-only 1 15
PopupCentered 3 13
PopupSheet 5 11
DesktopPopupPositioned 10 6

The mode was selected by which combination you passed, so a caller had to already know the platform to fill the interface in correctly. 81 files render Popup/FloatingMenu; 26 branch on platform within ±15 lines of it. reaction-tooltip.tsx renders two Kb.Popup elements in one component sharing only two props.

The platform rule was encoded three times independently. Changing one default inside Popup previously required editing 13 files to opt out.

Change

Three real modules with narrow interfaces — AnchoredPopup, Sheet, ModalCover — plus Popup kept as a small policy module picking a mode from intent, so the platform default lives in exactly one place and the 40+ menu one-liners are unchanged.

The intent set is 'menu' | 'dialog', derived by classifying all 22 real call sites, not invented. Sites whose behaviour never depended on platform now take a mode directly.

popup/index.tsx: 255 → 45 lines. The file-suffix seam is restored, so @gorhom/bottom-sheet and react-native-screens live only in sheet.native.tsx, and the raw <div> only in modal-cover.desktop.tsx — which also resolves a no-DOM-in-plain-.tsx violation.

Removed as provably dead

visible, mobileAnchored (2 sites → AnchoredPopup), and hideKeyboard's latent trap — it now exists only on the one mode that implements it. usePopup2's isMobile ? undefined : popupAnchor and min-writer-role's ref={isMobile ? null : popupAnchor} are both gone.

Behavior kept from master

  • visible — FloatingMenu already returned null for hidden non-modal menus before reaching Popup, and mode="modal" returned its contents directly, never reaching Popup at all. The guard stays if (!visible && mode !== 'modal'), so a modal menu still renders regardless of visible. Pinned by tests for both halves.
  • Unanchored desktop Toast — on master it fell through Popup to the centered cover. Toast now renders ModalCover itself when it has no anchor (only the kick-out confirm's "Kicked" toast); anchoring it would have given the positioner nothing to measure and rendered an invisible box.

Contract tightened

AnchoredPopup.attachTo is required, so an anchored popup with no anchor no longer type-checks. Popup's 'menu' intent keeps it optional and falls back to ModalCover when it's missing.

Fixed along the way

hideKeyboard never fired for its one caller (the role picker): native FloatingBox only dismissed the keyboard when the prop changed, and the caller mounts with true. It now dismisses on mount too.

Validation

lint:all clean — 0 bailed out, 0 whole-props deps, tsc clean both projects.
yarn test:unit — 255 suites / 2579 tests green.

ModalCover gets 8 tests and FloatingMenu 3 — modes that could not be tested at all while they were branches inside a dispatcher importing FullWindowOverlay at module scope.

Popup was one 255-line dispatcher behind a 16-field interface that
branched on runtime state into four unrelated presentations, each
reading a different subset of the props and silently ignoring the rest.
The platform rule was encoded independently in three places.

Split the presentations into three modules with narrow interfaces -
AnchoredPopup, Sheet, ModalCover - and keep Popup as a policy module
that picks one from an explicit intent ('menu' or 'dialog'). The
platform default now lives only there, so usePopup2 no longer gates the
anchor ref and min-writer-role no longer gates its ref either. The type
system rejects placement props on a mode that ignores them, and
hideKeyboard now exists only on the mode that implements it.

Popup no longer takes visible: every caller passed a literal true.
FloatingMenu was the exception - it let mode='modal' past its own
visibility guard and leaned on Popup to drop the hidden menu on the way
past, so that guard is now unconditional.

Sheet and ModalCover get file suffixes, so @gorhom/bottom-sheet and
react-native-screens leave the desktop bundle and the raw div leaves the
native one. ModalCover, previously untestable inside the dispatcher,
gets tests for its press and escape handling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rccpV5nLxxC5opF5xzrz7
@chrisnojima
chrisnojima force-pushed the nojima/HOTPOT-arch-05-popup-modes branch from d71d720 to 633433b Compare September 25, 2026 22:04
@chrisnojima
chrisnojima marked this pull request as ready for review September 25, 2026 23:11
Unanchored desktop Toast falls back to ModalCover as it did via Popup;
modal FloatingMenu ignores visible again. AnchoredPopup now requires
attachTo, Popup spreads its menu props, native FloatingBox dismisses the
keyboard on mount, and the unused default exports are gone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant